<?php

/**
 * Enum-like construct containing all opcodes defined in the WebSocket protocol
 
 * @author Chris
 *
 */
class WebSocketOpcode{
	const __default = 0;
	
	const ContinuationFrame = 0x00;
	const TextFrame = 0x01;
	const BinaryFrame = 0x02;
	
	const CloseFrame = 0x08;
	
	const PingFrame = 0x09;
	const PongFrame = 0x09;
	
	private function __construct(){
		
	}

	/**
	 * Check if a opcode is a control frame. Control frames should be handled internally by the server.
	 * @param int $type
	 */
	public static function isControlFrame($type){
		$controlframes = array(self::CloseFrame, self::PingFrame, self::PongFrame);
		
		return array_search($type, $controlframes) !== false;
	}
}


/**
 * Interface for WebSocket frames. One or more frames compose a message.
 * In the case of the Hixie protocol, a message contains of one frame only
 *
 * @author Chris
 */
interface IWebSocketFrame{
	/**
	 * Serialize the frame so that it can be send over a socket
	 * @return string Serialized binary string
	 */
	public function encode();
	
	/**
	 * Deserialize a binary string into a IWebSocketFrame
	 * @return string Serialized binary string
	 */
	public static function decode($string);
	
	/**
	 * @return string Payload Data inside the frame
	 */
	public function getData();
	
	/**
	 * @return int The frame type (opcode)
	 */
	public function getType();
	
	/**
	 * Create a frame by type and payload data
	 * @param int $type
	 * @param string $data
	 * 
	 * @return IWebSocketFrame
	 */
	public static function create($type, $data = null);
}

/**
 * HYBIE WebSocketFrame
 *
 * @author Chris
 *
 */
class WebSocketFrame implements IWebSocketFrame{
	// First Byte
	protected $FIN = 0;
	protected $RSV1 = 0;
	protected $RSV2 = 0;
	protected $RSV3 = 0;
	protected $opcode = WebSocketOpcode::TextFrame;
	
	// Second Byte
	protected $mask = 0;
	protected $payloadLength = 0;
	protected $maskingKey = 0;
	
	protected $payloadData = 0;
	
	private function __construct(){	}
	
	public static function create($type, $data = null){
		$o = new self();
		
		$o->FIN = true;
		$o->payloadData = $data;
		$o->payloadLength = $data != null ? strlen($data) : 0;
		$o->setType($type);
		
		
		return $o;
	}
	
	public function isMasked(){
		return $this->mask == 1;
	}
	
	protected function setType($type){
		$this->opcode = $type;
		
		if($type == WebSocketOpcode::CloseFrame)
			$this->mask = 1;
	}
	
	protected static function IsBitSet($byte, $pos)
	{
		return ($byte & pow(2,$pos)) > 0 ? 1 : 0;
	}
	
	protected static function rotMask($data, $key){
		$res = '';
		for($i = 0; $i < strlen($data); $i++){
			$j = $i % 4;
			
			$res .= chr(ord($data[$i]) ^ ord($key[$j]));
		}
		
		return $res;
	}
	
	public function getType(){
		return $this->opcode;
	}
	
	public function encode(){
		$this->payloadLength = strlen($this->payloadData);
		
		
		$firstByte = $this->opcode;
		
		$firstByte += 
			$this->FIN * 128
			+ $this->RSV1 * 64
			+ $this->RSV2 * 32
			+ $this->RSV3 * 16;
			
		$encoded = chr($firstByte);
		
		if($this->payloadLength <= 125){
			$secondByte = $this->payloadLength;
			$secondByte += $this->mask * 128;

			$encoded .= chr($secondByte);
		} else if($this->payloadLength <= 255*255 - 1){
			$secondByte = 126;
			$secondByte += $this->mask * 128;
			
			$encoded .= chr($secondByte).pack("n", $this->payloadLength);
		} else {
			// TODO: max length is now 32 bits instead of 64 !!!!!
			$secondByte = 127;
			$secondByte += $this->mask * 128;
			
			$encoded .= chr($secondByte);
			$encoded .= pack("N",0);
			$encoded .= pack("N",$this->payloadLength);
		}
		
		$key = 0;
		if($this->mask){
			$key = pack("N", rand(0, pow(255,4) - 1));
			$encoded .= $key;
			
		}
		
		if($this->payloadData)
			$encoded .= ($this->mask == 1) ? $this->rotMask($this->payloadData,$key) : $this->payloadData;
		
		return $encoded;
	}
	
	public static function decode($raw){
		$frame = new self();
	
		// Read the first two bytes, then chop them off
		list($firstByte, $secondByte) = substr($raw,0,2);	
		$raw = substr($raw,2);
			
		$firstByte = ord($firstByte);
		$secondByte = ord($secondByte);
		
		$frame->FIN = self::IsBitSet($firstByte, 7);
		$frame->RSV1 = self::IsBitSet($firstByte, 6);
		$frame->RSV2 = self::IsBitSet($firstByte, 5);
		$frame->RSV3 = self::IsBitSet($firstByte, 4);
		
		$frame->mask = self::IsBitSet($secondByte, 7);
		
		$frame->opcode = ($firstByte & 0x0F);
		
		$len = $secondByte & ~128;
		
		if($len <= 125)
			$frame->payloadLength = $len;
		elseif($len == 126){
			list($frame->payloadLength) = unpack("nfirst", $raw);
			$raw = substr($raw,2);
		} elseif($len = 127) {
			list($frame->payloadLength) = unpack("nfirst", $raw);
			$raw = substr($raw,4);
		}
		if($frame->mask){
			$frame->maskingKey = substr($raw,0,4);
			$raw = substr($raw,4);
		}/*
		if(strlen($raw) != $frame->payloadLength){
			//$raw = substr($raw,0,$frame->payloadLength);
			throw new WebSocketFrameSizeMismatch($frame);
		}*/
		
		$rest = false;
		if(strlen($raw) != $frame->payloadLength){
			$rest = substr($raw, $frame->payloadLength);
			$raw = substr($raw,0,$frame->payloadLength);
		}
		
		if($frame->mask)
			$frame->payloadData = self::rotMask($raw, $frame->maskingKey);
		else $frame->payloadData = $raw;
		
		$frames = array($frame);
		if($rest){
			$nextFrames = WebSocketFrame::decode($rest);
			foreach($nextFrames as $key=>$value){
				array_push($frames, $value);
			}
		}
		return $frames;
	}
	
	public function isFinal(){
		return $this->FIN == 1;
	}
	
	public function getData(){
		return $this->payloadData;
	}
}

class WebSocketFrame76 implements IWebSocketFrame{
	public $payloadData = '';
	protected $opcode = WebSocketOpcode::TextFrame;
	
	public static function create($type, $data = null){
		$o = new self();
		
		$o->payloadData = $data;
		
		return $o;
	}
	
	public function encode(){
		return chr(0).$this->payloadData.chr(255);
	}
	
	public function getData(){
		return $this->payloadData;
	}
	
	public function getType(){
		return $this->opcode;
	}
	
	public static function decode($str){
		$o = new self();
		$o->payloadData = substr($str, 1, strlen($str) - 2);
		
		return $o;
	}
	
}